Java JWT

Java版本的对于JSON Web Token(即 JWT )的实现

安装导入依赖

Maven

1
2
3
4
5
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.13.0</version>
</dependency>

Gradle

1
implementation 'com.auth0:java-jwt:3.13.0'

支持的算法

JWS Algorithm Description
HS256 HMAC256 HMAC with SHA-256
HS384 HMAC384 HMAC with SHA-384
HS512 HMAC512 HMAC with SHA-512
RS256 RAS256 RSASSA-PKCS1-v1_5 with SHA-256
RS384 RAS384 RSASSA-PKCS1-v1_5 with SHA-384
RS512 RAS512 RSASSA-PKCS1-v1_5 with SHA-512
ES256 ECDSA256 ECDSA with curve P-256 and SHA-256
ES256K ECDSA256 ECDSA with curve secp256k1 and SHA-256
ES384 ECDSA384 ECDSA with curve P-384 and SHA-384
ES512 ECDSA512 ECDSA with curve P-521 and SHA-512

用法

挑选算法

算法决定了token怎么被签名和认证。

使用静态的 secrets 或 keys

1
2
3
4
5
6
7
//HMAC
Algorithm algorithmHS = Algorithm.HMAC256("secret");

//RSA
RSAPublicKey publicKey = //Get the key instance
RSAPrivateKey privateKey = //Get the key instance
Algorithm algorithmRS = Algorithm.RSA256(publicKey, privateKey);

使用一个 KeyProvider

通过使用 KeyProvider,你可以在运行时改变用RSA或者ECDSA算法验证或者签署新 token 时使用的 key ,通过使用 RSAKeyProvider 或者 ECDSKKeyProvider 等方法可以做到

  • getPublicKeyById(String kid): 在 token 签名验证时调用,返回用于验证当前 token 的 key 。如果 key rotation 正在被使用(如: JWK),那么它可以通过 id 取回正确的 rotation key ,或者始终返回同一个 key
  • getPrivateKey(): 在签署 token 时调用,会返回将要用于签署 JWT 的 key。
  • getPrivateKeyId(): 在签署 token 时调用,会返回可以确认 getPrivateKey() 方法返回的 key 的 id 。这个值相较于 JWTCreator.Builder#withKeyId(String) 方法中的值优先级高。如果你不需要设置一个 kid 值,避免使用 KeyProvider 声明算法。

下面这个例子展示了 KeyProvider 如何在 JwkStore 一个梦幻的JWK SET 实现中工作。

例如简单的使用 JWKS 的 key rotation ,使用 jwks-rsa-java library。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
final JwkStore jwkStore = new JwkStore("{JWKS_FILE_HOST}");
final RSAPrivateKey privateKey = //Get the key instance
final String privateKeyId = //Create an Id for the above key

RSAKeyProvider keyProvider = new RSAKeyProvider() {
@Override
public RSAPublicKey getPublicKeyById(String kid) {
//Received 'kid' value might be null if it wasn't defined in the Token's header
RSAPublicKey publicKey = jwkStore.get(kid);
return (RSAPublicKey) publicKey;
}

@Override
public RSAPrivateKey getPrivateKey() {
return privateKey;
}

@Override
public String getPrivateKeyId() {
return privateKeyId;
}
};

Algorithm algorithm = Algorithm.RSA256(keyProvider);
//Use the Algorithm to create and verify JWTs.

创建并签署一个 Token

首先你必须调用 JWT.create() 方法创建一个 JWTCreator实例。利用这个构筑器确定你需要使用的 token 的样式。最后,通过 sign() 方法获得 String 类型的 token,把该 token 传递给算法的实例

  • 例:使用 HS256 算法
1
2
3
4
5
6
7
8
try {
Algorithm algorithm = Algorithm.HMAC256("secret");
String token = JWT.create()
.withIssuer("auth0")
.sign(algorithm);
} catch (JWTCreationException exception){
//Invalid Signing configuration / Couldn't convert Claims.
}
  • 例:使用 RS256 算法
1
2
3
4
5
6
7
8
9
10
RSAPublicKey publicKey = //Get the key instance
RSAPrivateKey privateKey = //Get the key instance
try {
Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey);
String token = JWT.create()
.withIssuer("auth0")
.sign(algorithm);
} catch (JWTCreationException exception){
//Invalid Signing configuration / Couldn't convert Claims.
}

如果一个样式无法被转化为 JSON 格式,或者用在签署过程中的 Key 是无效的,那么一个 JWTCreationException 异常将会产生

验证一个 Token

首先需要做的,是通过调用 JWT.require 方法创建一个 JWTVerifier的实例并把算法的实例传递给它。如果你需要 token 携带特殊的样式的值,使用构筑器去定义它们。通过 build() 方法返回的实例是可以被重复使用的,因此你可以每次定义实例去验证不同的 token,最后使用 verifier.verify() 方法传递 token。

  • 例:使用 HS256 算法
1
2
3
4
5
6
7
8
9
10
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE";
try {
Algorithm algorithm = Algorithm.HMAC256("secret");
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("auth0")
.build(); //Reusable verifier instance
DecodedJWT jwt = verifier.verify(token);
} catch (JWTVerificationException exception){
//Invalid signature/claims
}
  • 例:使用 RS256 算法
1
2
3
4
5
6
7
8
9
10
11
12
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE";
RSAPublicKey publicKey = //Get the key instance
RSAPrivateKey privateKey = //Get the key instance
try {
Algorithm algorithm = Algorithm.RSA256(publicKey, privateKey);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("auth0")
.build(); //Reusable verifier instance
DecodedJWT jwt = verifier.verify(token);
} catch (JWTVerificationException exception){
//Invalid signature/claims
}

如果 token 携带无效的签名,或者宣称的样式不存在,一个 JWTVerificationException 异常将会产生

有效时间(Time Validation)

JWK token 可以携带 日期数字字段(DateNumber),以用来证实如下信息

  • token 在过去的某一刻被颁布 "iat" < TODAY
  • token 仍未失效(未超过有效期时间)"exp" > TODAY
  • token 可能已经被使用过了 "nbf" < TODAY

当确认自动产生的的 token 有效期时,如果值是无效的,那么会导致抛出 JWTVerificationException 异常。如果任何之前的域丢失,它们不会在确认 token 时被考虑。

为了指定一个 leeway window ,确定在哪些地方 token 应该被确认是否有效,可以使用 在 JWTVerifier 构筑器中的 acceptLeeway() 方法然后传递一个确定的毫秒值(seconds value)。这对任何以上列出的都生效。

1
2
3
JWTVerifier verifier = JWT.require(algorithm)
.acceptLeeway(1) // 1 sec for nbf, iat and exp
.build();

也可以通过给定的日期样式(Date claim),和仅重写那个默认的描述来区分一个特殊的值

1
2
3
4
JWTVerifier verifier = JWT.require(algorithm)
.acceptLeeway(1) //1 sec for nbf and iat
.acceptExpiresAt(5) //5 secs for exp
.build();

如果需要测试你的 lib/abb 的运行状况,可以通过把 Verification 实例转型为 BaseVerificaion 来访问 可以接受自定义的 Clock 的 verification.build() 方法。

token的解码(Decode a Token)

1
2
3
4
5
6
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE";
try {
DecodedJWT jwt = JWT.decode(token);
} catch (JWTDecodeException exception){
//Invalid token
}

如果 token 使用无效的语法或者它的头部(header)或者 携带部分(payload)不是 JSON 格式,一个 JWTDecodeException异常将会产生

token的头部定义(Header Claims)

算法部分 Algorithm(“alg”)

如果定义则返回算法类型,否则返回 null

1
String algorithm = jwt.getAlgorithm();

类型部分 Type(“typ”)

如果未定义则返回 Type ,否则返回 null

1
String type = jwt.getType();

内容类型 Content Type(“cty”)

如果未定义则返回 Content Type,否则返回 null

1
String contentType = jwt.getContentType();

Key Id(“kid”)

如果未定义则返回 Key Id,否则返回 null

1
String keyId = jwt.getKeyId();

私有自定义部分 Private Claims

可以通过 getHeaderClaim() 方法在 token 的头部添加额外的 Claim 和 Claim name。就算无法被找到,一个 Claim也会被返回。可以通过 claim.isNull() 方法检查 Claim 得值是否为空

1
Claim claim = jwt.getHeaderClaim("owner");

当使用 JWT.create() 方法创建 Token时,你可以通过 withHeader() 以 map 集合方式传递一系列 claim 来指定 header claims

1
2
3
4
5
Map<String, Object> headerClaims = new HashMap();
headerClaims.put("owner", "auth0");
String token = JWT.create()
.withHeader(headerClaims)
.sign(algorithm);

token的携带部分定义(Payload Claims)

发行人 Issuer(“iss”)

如果未定义则返回 Issuer ,否则返回 null

1
String issuer = jwt.getIssuer();

主题 Subject(“sub”)

如果未定义则返回 Subject value ,否则返回 null

1
String subject = jwt.getSubject();

Audience(“aud”)

如果未定义则返回 Audience value ,否则返回 null

1
List<String> audience = jwt.getAudience();

过期时间 Expiration Time(“exp”)

如果未定义则返回 Expiration Time value ,否则返回 null

1
Date expiresAt = jwt.getExpiresAt();

Not Before(“nbf”)

如果未定义则返回 Not Before value ,否则返回 null

1
Date notBefore = jwt.getNotBefore();

Issued At(“iat”)

如果未定义则返回 Issued At value ,否则返回 null

1
Date issuedAt = jwt.getIssuedAt();

JWT Id(“jti”)

如果未定义则返回 JWT ID value ,否则返回 null

1
String id = jwt.getId();

Private Claims

通过 getClaims() 或着 getClaim() 方法并传递 Claim name 可以获得 token 的 Payload 中额外的 Claim。一个 Claim通常会被返回,除非它不存在。可以通过调用 claim.isNull() 来检查 Claim 的值是否为 null 。

1
2
Map<String, Claim> claims = jwt.getClaims();    //Key is the Claim name
Claim claim = claims.get("isAdmin");

或者

1
Claim claim = jwt.getClaim("isAdmin");

当通过 JWT.create() 创建 token 时,也可以通过调用 withClaim() 方法并传递 Claim 的 name 与 value 来指定一个 Claim

1
2
3
4
String token = JWT.create()
.withClaim("name", 123)
.withArrayClaim("array", new Integer[]{1, 2, 3})
.sign(algorithm);

Claim Class

Claim class 是 Claim values 的封装。它允许你以不同的类型(different class types)获取 claim 。

基本方法

  • **asBoolean()**:返回Boolean类型的值,如不能转型则返回 null
  • **asInt()**:返回 Integer 类型的值,如不能转型则返回 null
  • **asDouble()**:返回 Double 类型的值,如不能转型则返回 null
  • asLong:返回 Long 类型的值,如不能转型则返回 null
  • asString:返回 String 类型的值,如不能转型则返回 null
  • **asDate()**:返回 Date 类型的值,如不能转型则返回 null 。必须是一个 NumericDate(Unix Epoch/Timestamp)。注:JWT Standard 指定了所有的 NumericDate values 必须以秒(seconds)为单位